# Purpose: Implements the actual web automation actions using Playwright.
# Changes:
# - Explicitly passed `observation_summary=None` during PageObservation instantiation to satisfy Pyright.

import logging
import json
from typing import Dict, Any, List, Optional
from playwright.async_api import Page, Locator, TimeoutError, Error as PlaywrightError

# Use relative imports
from .manager import get_current_page, _get_web_config
from ...core.exceptions import SkillError
from ...schemas.mcp.web_automation import PageObservation, InteractableElement
from ...core.logging_setup import get_logger

logger = get_logger(__name__)

# --- JavaScript for Observation ---
# (JS remains the same)
FIND_AND_TAG_INTERACTABLES_JS = """
(maxElements) => {
    const interactableSelectors = [
        'a[href]', 'button:not([disabled])', 'input:not([type="hidden"]):not([disabled])',
        'textarea:not([disabled])', 'select:not([disabled])', '[role="button"]:not([disabled])',
        '[role="link"]', '[role="checkbox"]:not([disabled])', '[role="radio"]:not([disabled])',
        '[role="option"]', '[role="menuitem"]', '[role="tab"]',
        '[contenteditable="true"]:not([disabled])', 'details summary',
    ];
    const elements = []; let elementCounter = 0; const processedElements = new Set();
    document.querySelectorAll('[data-agent-id]').forEach(el => el.removeAttribute('data-agent-id'));

    document.querySelectorAll(interactableSelectors.join(', ')).forEach(el => {
        if (processedElements.has(el)) return;
        const rect = el.getBoundingClientRect();
        const isInViewport = rect.top < window.innerHeight && rect.bottom >= 0 &&
                             rect.left < window.innerWidth && rect.right >= 0;
        const hasSize = rect.width > 1 && rect.height > 1;
        let isVisible = false; let style = null;
        try {
             style = window.getComputedStyle(el);
             isVisible = style.visibility !== 'hidden' && style.display !== 'none' &&
                         style.opacity !== '0' && el.offsetParent !== null;
        } catch (e) { /* Ignore */ }

        if (isInViewport && hasSize && isVisible) {
            elementCounter++; const agentId = `el${elementCounter}`;
            el.setAttribute('data-agent-id', agentId); processedElements.add(el);
            const getText = (elem) => (elem.getAttribute('aria-label') || elem.textContent || elem.innerText || '').trim().replace(/\s+/g, ' ').substring(0, 150);
            const getAttr = (elem, attr, maxLen = 200) => { const val = elem.getAttribute(attr); return val ? String(val).substring(0, maxLen) : null; };
            const getValue = (elem, maxLen = 100) => (elem.value !== undefined && elem.value !== null) ? String(elem.value).substring(0, maxLen) : null;

            const attributes = {
                'tag_name': el.tagName.toLowerCase(), 'text': getText(el) || null,
                'aria-label': getAttr(el, 'aria-label'), 'role': getAttr(el, 'role'),
                'placeholder': getAttr(el, 'placeholder'), 'value': getValue(el),
                'type': getAttr(el, 'type'), 'href': getAttr(el, 'href'),
                'title': getAttr(el, 'title'), 'data-testid': getAttr(el, 'data-testid'),
                'name': getAttr(el, 'name'),
            };
            const filteredAttributes = Object.fromEntries(Object.entries(attributes).filter(([_, v]) => v !== null && v !== ''));
            elements.push({ agent_id: agentId, tag_name: el.tagName.toLowerCase(), text: attributes['text'], attributes: filteredAttributes });
        }
    });
    return elements.slice(0, maxElements);
}
"""

# --- Skill Implementations ---

async def observe_page(focus_selector: Optional[str] = None) -> PageObservation:
    """Observes the current page state using Playwright."""
    page: Page = get_current_page() # Raises SkillError if page not available
    config = _get_web_config()
    max_text_len = config.get("max_observation_text_length", 3000)
    max_elements = config.get("max_interactable_elements", 75)
    skill_name = "observe_page"
    logger.info(f"[{skill_name.upper()}] Starting observation. Focus: '{focus_selector or 'body'}'")

    # Variables to store gathered data
    page_url: Optional[str] = None
    page_title: Optional[str] = None
    focused_text_content: Optional[str] = None
    interactable_elements_list: List[InteractableElement] = []
    errors_encountered: List[str] = []
    critical_error: Optional[Exception] = None

    try:
        # 1. Get URL and Title safely
        try:
            page_url = page.url
            page_title = await page.title() # Removed invalid timeout argument
        except (PlaywrightError, TimeoutError) as e:
            logger.warning(f"[{skill_name.upper()}] Failed to get URL/Title: {e}")
            errors_encountered.append(f"Failed to get URL/Title: {type(e).__name__}") # Log type

        # 2. Execute JS for elements
        try:
            logger.debug(f"[{skill_name.upper()}] Executing element tagging JS (limit: {max_elements})...")
            js_script = f"({FIND_AND_TAG_INTERACTABLES_JS})({max_elements})"
            elements_data = await page.evaluate(js_script)
            if isinstance(elements_data, list):
                valid_elements = []
                for i, data in enumerate(elements_data):
                    try:
                        valid_elements.append(InteractableElement(**data))
                    except Exception as val_err: # Catch Pydantic validation errors per element
                        logger.warning(f"[{skill_name.upper()}] Error validating element data #{i+1}: {val_err}. Data: {data}")
                        errors_encountered.append(f"Element #{i+1} validation error")
                interactable_elements_list = valid_elements
                logger.info(f"[{skill_name.upper()}] Found and tagged {len(interactable_elements_list)} valid elements.")
            else:
                logger.warning(f"[{skill_name.upper()}] Element tagging JS did not return list: {type(elements_data)}")
                errors_encountered.append("Element tagging JS failed.")
        except PlaywrightError as e:
            logger.error(f"[{skill_name.upper()}] Error executing element tagging JS: {e}", exc_info=False)
            errors_encountered.append(f"Element tagging JS error: {type(e).__name__}")

        # 3. Extract Text Content safely
        text_selector = focus_selector if focus_selector else "body"
        try:
            logger.debug(f"[{skill_name.upper()}] Extracting text from: '{text_selector}'...")
            locator = page.locator(text_selector).first
            await locator.wait_for(state="attached", timeout=5000)
            raw_text = await locator.inner_text(timeout=10000)
            cleaned_text = ' '.join(raw_text.split()).strip()
            if len(cleaned_text) > max_text_len:
                focused_text_content = cleaned_text[:max_text_len] + f"... (truncated)"
            else:
                focused_text_content = cleaned_text if cleaned_text else None
            logger.debug(f"[{skill_name.upper()}] Extracted text length: {len(focused_text_content or '')}")
        except (TimeoutError, PlaywrightError):
            logger.warning(f"[{skill_name.upper()}] Failed to get text content for '{text_selector}'.")
            focused_text_content = None
        except Exception as e:
            logger.error(f"[{skill_name.upper()}] Unexpected error getting text: {e}", exc_info=True)
            errors_encountered.append(f"Text extraction error: {type(e).__name__}")

    except SkillError as e:
         # Error likely from get_current_page()
         logger.error(f"[{skill_name.upper()}] Prerequisite skill error: {e}")
         critical_error = e # Store critical error
    except Exception as e:
        logger.error(f"[{skill_name.upper()}] Unexpected critical error during observation setup: {e}", exc_info=True)
        critical_error = e # Store critical error

    # 4. Instantiate PageObservation with gathered data *at the end*
    final_error_msg_parts = errors_encountered
    if critical_error:
        final_error_msg_parts.insert(0, f"Critical observation error: {type(critical_error).__name__}")

    final_error_msg = "; ".join(final_error_msg_parts) if final_error_msg_parts else None

    if final_error_msg:
        logger.warning(f"[{skill_name.upper()}] Observation completed with errors: {final_error_msg}")
    else:
        logger.info(f"[{skill_name.upper()}] Observation completed successfully.")

    # Explicitly pass None for optional fields if not set
    return PageObservation(
        url=page_url,
        title=page_title,
        focused_text=focused_text_content,
        interactable_elements=interactable_elements_list,
        error_message=final_error_msg,
        observation_summary=None # Explicitly pass None for the missing field
    )

# --- Other actions (navigate_to_url, click_element, type_text, get_text_content) ---
# Re-paste the corrected versions from the previous step as they were okay
# (Or verify them again based on the latest state if needed)

async def navigate_to_url(url: str) -> str:
    """Navigates the browser page to the specified URL using Playwright."""
    page: Page = get_current_page()
    skill_name = "navigate_to_url"
    logger.info(f"[{skill_name.upper()}] Navigating to: {url}")
    try:
        if not url or not isinstance(url, str): raise ValueError("URL must be a non-empty string.")
        if not url.startswith(("http://", "https://")):
             logger.debug(f"Prepending 'https://' to URL: {url}")
             url = "https://" + url
        response = await page.goto(url, wait_until="domcontentloaded")
        status = response.status if response else 0
        logger.info(f"[{skill_name.upper()}] Navigation to '{url}' completed. Status: {status}")
        if not response or not response.ok:
            error_msg = f"Navigation failed with HTTP status {status}"
            if status == 0: error_msg = "Navigation failed (no response)"
            raise SkillError(skill_name=skill_name, message=error_msg)
        await page.wait_for_timeout(250)
        return f"Successfully navigated to {url} (Status: {status})"
    except TimeoutError as e:
        logger.error(f"[{skill_name.upper()}] Timeout error navigating to {url}", exc_info=False)
        raise SkillError(skill_name=skill_name, message=f"Timeout navigating to '{url}'", original_exception=e) from e
    except PlaywrightError as e:
        logger.error(f"[{skill_name.upper()}] Playwright error navigating to {url}: {e}", exc_info=False)
        msg = f"Navigation error: {e}"
        if "net::ERR_" in str(e): msg = f"Navigation failed (Network error: {e})"
        raise SkillError(skill_name=skill_name, message=msg, original_exception=e) from e
    except (ValueError, TypeError) as e:
        logger.error(f"[{skill_name.upper()}] Invalid argument for navigation: {e}", exc_info=False)
        raise SkillError(skill_name=skill_name, message=f"Invalid URL provided: {e}", original_exception=e) from e
    except SkillError as e: raise e
    except Exception as e:
        logger.error(f"[{skill_name.upper()}] Unexpected error navigating to {url}: {e}", exc_info=True)
        raise SkillError(skill_name=skill_name, message=f"Unexpected navigation error: {e}", original_exception=e) from e

async def click_element(element_id: str) -> str:
    """Clicks an element identified by 'data-agent-id' using Playwright."""
    page: Page = get_current_page()
    config = _get_web_config()
    visibility_timeout_ms = config.get("visibility_timeout", 10) * 1000
    click_timeout_ms = config.get("click_timeout", 15) * 1000
    skill_name = "click_element"
    selector = f'[data-agent-id="{element_id}"]'
    logger.info(f"[{skill_name.upper()}] Attempting click on element_id='{element_id}' (selector: {selector})")
    try:
        if not element_id or not isinstance(element_id, str): raise ValueError("Element ID must be a non-empty string.")
        locator = page.locator(selector)
        try:
             logger.debug(f"[{skill_name.upper()}] Waiting for element '{element_id}' to be attached (short)...")
             await locator.wait_for(state="attached", timeout=3000)
        except TimeoutError:
              logger.error(f"[{skill_name.upper()}] Element ID '{element_id}' not found (timed out waiting for attach).")
              raise SkillError(skill_name=skill_name, message=f"Element ID '{element_id}' not found on the page.")
        count = await locator.count()
        if count == 0: raise SkillError(skill_name=skill_name, message=f"Element ID '{element_id}' not found after attach check.")
        elif count > 1:
             logger.warning(f"[{skill_name.upper()}] Multiple elements ({count}) found for agent_id '{element_id}'. Clicking the first one.")
             locator = locator.first
        logger.debug(f"[{skill_name.upper()}] Waiting for element '{element_id}' to be visible (timeout: {visibility_timeout_ms}ms)...")
        await locator.wait_for(state="visible", timeout=visibility_timeout_ms)
        logger.debug(f"[{skill_name.upper()}] Performing click action (timeout: {click_timeout_ms}ms)...")
        await locator.click(timeout=click_timeout_ms)
        await page.wait_for_timeout(500)
        new_url = page.url
        logger.info(f"[{skill_name.upper()}] Successfully clicked element '{element_id}'. Current URL: {new_url}")
        return f"Successfully clicked element '{element_id}'."
    except TimeoutError as e:
        logger.error(f"[{skill_name.upper()}] Timeout error clicking element '{element_id}': {e}", exc_info=False)
        if "waiting for element to be visible" in str(e).lower(): msg = f"Timeout waiting for element '{element_id}' to become visible."
        elif "waiting for selector" in str(e).lower(): msg = f"Timeout finding element '{element_id}'."
        else: msg = f"Timeout during click operation on element '{element_id}'."
        raise SkillError(skill_name=skill_name, message=msg, original_exception=e) from e
    except PlaywrightError as e:
         logger.error(f"[{skill_name.upper()}] Playwright error clicking element '{element_id}': {e}", exc_info=False)
         msg = f"Error clicking element '{element_id}': {e}"
         if "element is not visible" in str(e).lower(): msg = f"Element '{element_id}' found but not visible/clickable."
         if "strict mode violation" in str(e).lower(): msg = f"Ambiguous match for element ID '{element_id}'."
         raise SkillError(skill_name=skill_name, message=msg, original_exception=e) from e
    except (ValueError, TypeError) as e:
         logger.error(f"[{skill_name.upper()}] Invalid argument for click: {e}", exc_info=False)
         raise SkillError(skill_name=skill_name, message=f"Invalid element ID provided: {e}", original_exception=e) from e
    except SkillError as e: raise e
    except Exception as e:
        logger.error(f"[{skill_name.upper()}] Unexpected error clicking element '{element_id}': {e}", exc_info=True)
        raise SkillError(skill_name=skill_name, message=f"Unexpected click error: {e}", original_exception=e) from e


async def type_text(element_id: str, text_to_type: str, clear_first: bool = True) -> str:
    """Types text into an element identified by 'data-agent-id' using Playwright."""
    page: Page = get_current_page()
    config = _get_web_config()
    visibility_timeout_ms = config.get("visibility_timeout", 10) * 1000
    type_delay_ms = config.get("type_delay", 50)
    skill_name = "type_text"
    selector = f'[data-agent-id="{element_id}"]'
    log_text = (text_to_type[:50] + '...') if len(text_to_type) > 50 else text_to_type
    logger.info(f"[{skill_name.upper()}] Attempting to type into element_id='{element_id}'. Text: '{log_text}'")

    try:
        if not element_id or not isinstance(element_id, str): raise ValueError("Element ID must be a non-empty string.")
        if not isinstance(text_to_type, str): raise ValueError("Text to type must be a string.")

        locator = page.locator(selector)
        try:
             logger.debug(f"[{skill_name.upper()}] Waiting for element '{element_id}' to be attached (short)...")
             await locator.wait_for(state="attached", timeout=3000)
        except TimeoutError:
              logger.error(f"[{skill_name.upper()}] Element ID '{element_id}' not found for typing.")
              raise SkillError(skill_name=skill_name, message=f"Element ID '{element_id}' not found for typing.")

        count = await locator.count()
        if count == 0: raise SkillError(skill_name=skill_name, message=f"Element ID '{element_id}' disappeared after attach check.")
        elif count > 1:
             logger.warning(f"[{skill_name.upper()}] Multiple elements ({count}) found for agent_id '{element_id}'. Targeting first.")
             locator = locator.first

        # Actions like fill/type implicitly wait for element to be visible and enabled.
        # No need for explicit wait_for(state="editable")
        if clear_first:
            logger.debug(f"[{skill_name.upper()}] Clearing element '{element_id}' before typing.")
            await locator.fill("", timeout=visibility_timeout_ms)

        logger.debug(f"[{skill_name.upper()}] Typing text with delay {type_delay_ms}ms...")
        await locator.type(text_to_type, delay=type_delay_ms, timeout=visibility_timeout_ms * 3)

        logger.info(f"[{skill_name.upper()}] Successfully typed text into element '{element_id}'.")
        return f"Successfully typed text into element '{element_id}'."

    except TimeoutError as e:
        logger.error(f"[{skill_name.upper()}] Timeout error typing into element '{element_id}': {e}", exc_info=False)
        if "waiting for element to be visible" in str(e).lower(): msg = f"Timeout waiting for '{element_id}' to be visible for typing."
        elif "waiting for selector" in str(e).lower(): msg = f"Timeout finding element '{element_id}' for typing."
        else: msg = f"Timeout during typing operation into element '{element_id}'."
        raise SkillError(skill_name=skill_name, message=msg, original_exception=e) from e
    except PlaywrightError as e:
        logger.error(f"[{skill_name.upper()}] Playwright error typing into element '{element_id}': {e}", exc_info=False)
        msg = f"Error typing into element '{element_id}': {e}"
        if "strict mode violation" in str(e).lower(): msg = f"Ambiguous match for element ID '{element_id}' for typing."
        raise SkillError(skill_name=skill_name, message=msg, original_exception=e) from e
    except (ValueError, TypeError) as e:
         logger.error(f"[{skill_name.upper()}] Invalid argument for type: {e}", exc_info=False)
         raise SkillError(skill_name=skill_name, message=f"Invalid arguments provided: {e}", original_exception=e) from e
    except SkillError as e: raise e
    except Exception as e:
        logger.error(f"[{skill_name.upper()}] Unexpected error typing into element '{element_id}': {e}", exc_info=True)
        raise SkillError(skill_name=skill_name, message=f"Unexpected typing error: {e}", original_exception=e) from e

async def get_text_content(selector: Optional[str] = None) -> str:
    """Gets visible text content from selector using Playwright."""
    # (No changes needed from previous corrected version)
    page: Page = get_current_page()
    config = _get_web_config()
    max_len = config.get("max_observation_text_length", 3000)
    target_selector = selector if selector else "body"
    skill_name = "get_text_content"
    logger.info(f"[{skill_name.upper()}] Getting text content for selector: '{target_selector}'")
    try:
        if selector is not None and not isinstance(selector, str): raise ValueError("Selector must be a string or None.")
        locator = page.locator(target_selector)
        try: await locator.first.wait_for(state="attached", timeout=5000)
        except TimeoutError:
             logger.warning(f"[{skill_name.upper()}] No elements found matching '{target_selector}'.")
             return f"No elements found matching selector '{target_selector}'."
        all_texts = await locator.all_text_contents()
        if not all_texts:
             logger.info(f"[{skill_name.upper()}] No text content found for '{target_selector}'.")
             return f"No text content found for selector '{target_selector}'."
        raw_text = " ".join(all_texts)
        content = ' '.join(raw_text.split()).strip()
        original_len = len(content)
        if original_len == 0:
             logger.info(f"[{skill_name.upper()}] Elements found for '{target_selector}', but contained no text.")
             return f"Elements found for '{target_selector}', but contained no text."
        if original_len > max_len: content = content[:max_len] + f"... (truncated, original length: {original_len})"
        logger.info(f"[{skill_name.upper()}] Retrieved text content (length: {len(content)}) for '{target_selector}'.")
        return content
    except TimeoutError as e:
        logger.error(f"[{skill_name.upper()}] Timeout getting text for '{target_selector}'.", exc_info=False)
        raise SkillError(skill_name=skill_name, message=f"Timeout getting text for '{target_selector}'", original_exception=e) from e
    except PlaywrightError as e:
        logger.error(f"[{skill_name.upper()}] Playwright error getting text for '{target_selector}': {e}", exc_info=False)
        raise SkillError(skill_name=skill_name, message=f"Error getting text for '{target_selector}': {e}", original_exception=e) from e
    except (ValueError, TypeError) as e:
         logger.error(f"[{skill_name.upper()}] Invalid argument for get text: {e}", exc_info=False)
         raise SkillError(skill_name=skill_name, message=f"Invalid arguments provided: {e}", original_exception=e) from e
    except SkillError as e: raise e
    except Exception as e:
        logger.error(f"[{skill_name.upper()}] Unexpected error getting text for '{target_selector}': {e}", exc_info=True)
        raise SkillError(skill_name=skill_name, message=f"Unexpected error getting text: {e}", original_exception=e) from e